package nl.Viewer.ViewManager.Controllers;

import java.awt.BorderLayout;
import java.awt.Cursor;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;

import javax.swing.JPanel;

import nl.Viewer.Application;
import nl.Viewer.SerializedObjects.DataObject;
import nl.Viewer.SerializedObjects.Modality;
import nl.Viewer.SerializedObjects.Parameter;
import nl.Viewer.SerializedObjects.ParameterValue;
import nl.Viewer.SerializedObjects.Visualization;
import nl.Viewer.Sessions.ClientSideSession;
import nl.Viewer.ViewManager.ViewStatusBar;
import nl.Viewer.ViewManager.Models.DataModel;
import nl.Viewer.ViewManager.Models.DataStores.DataStore;
import nl.Viewer.ViewManager.Objects.OutputVisualizationTuple;
import nl.Viewer.ViewManager.Views.GenericView;
import nl.Viewer.ViewManager.Views.ParameterView.DefaultParameterView;

/**
 * A generic class for managing all updates on views. This object also subs as the contentpanel of the view frame. In
 * the analogy of the MVC (Model-View-Controller) paradigm, this is the class on which all controllers are based. A
 * controller knows all visualization elements like: the chosen modality, visualization and output, as well as the
 * dataModel (containing the parameters and data stores). Besides its own content, the controller may also have an
 * associated orientation- and a parameter panel.
 * 
 * @author Jeroen Verschuur
 * 
 */

public abstract class GenericController extends JPanel implements Runnable {
   private static final long serialVersionUID = -6008896287961725552L;
   public final Visualization     visualization;
   public final Modality          modality;
   public final ClientSideSession session;
   public final String            outputType;
   protected ViewStatusBar        statusBar             = new ViewStatusBar();

   private volatile boolean       needsUpdate           = false;
   private volatile boolean       shuttingDown          = false;

   protected DataModel            dataModel             = new DataModel();

   protected GenericView          visualizationRenderer = null;
   protected DefaultParameterView parameterView         = null;
   protected GenericView          orientationView       = null;

   private Object                 updateLockObject      = new Object();

   /**
    * Note 1) if this constructor signature is ever changed, be sure to modify the constructor signature in the
    * newInstance method below!
    * 
    * Note 2) this class is abstract, so it cannot be instantiated without being extended
    * 
    * @param v
    * @param modality
    * @param session
    * @param outputType
    */

   public GenericController( Visualization v, Modality modality, ClientSideSession session, String outputType ) {
      super( new BorderLayout() );

      this.visualization = v;
      this.modality = modality;
      this.session = session;
      this.outputType = outputType;
   }

   public final void run() {
      needsUpdate = true; // force initial update

      while( !shuttingDown ) {
         synchronized( updateLockObject ) {
            while( !shuttingDown && !needsUpdate ) {
               try {
                  updateLockObject.wait();
               }
               catch( InterruptedException ie ) {
                  // ignore
               }
            }
         }

         while( !shuttingDown && needsUpdate ) {
            try {
               statusBar.setProgress( "Requesting render", 0 );

               // fetch all renderer specific parameters (view size, azimuth, etc.) from all views that may need it
               if( visualizationRenderer != null ) {
                  visualizationRenderer.setPreRenderParameters();
               }
               if( orientationView != null )
                  orientationView.setPreRenderParameters();

               if( parameterView != null )
                  parameterView.setPreRenderParameters();

               synchronized( this ) {
                  needsUpdate = false;
               }

               // request a new render with the current state of the parameters
               session.requestRender( getParameterSnapshot() );

               System.out.println( "Render requested" );
            }
            catch( Exception e ) {
               e.printStackTrace();
            }
         }
      }
   }

   public synchronized void requestNewRender() {
      // just signal to the render thread that we would like an update
      needsUpdate = true;
      synchronized( updateLockObject ) {
         updateLockObject.notify();
      }
   }

   public void start() {
      new Thread( this ).start();
   }

   public void shutdown() {
      // shutdown the controller thread
      shuttingDown = true;
      synchronized( updateLockObject ) {
         updateLockObject.notify();
      }

      // shutdown the visualization renderer
      if( visualizationRenderer != null ) {
         visualizationRenderer.shutdown();
      }

      // shutdown the parameter view
      if( parameterView != null ) {
         parameterView.shutdown();
      }

      // shutdown the orientation view
      if( orientationView != null ) {
         orientationView.shutdown();
      }

      // remove the dataModel data
      if( dataModel != null ) {
         dataModel.cleanup();
         dataModel = null;
      }

      // shutdown the session
      session.shutdown();
   }

   public static GenericController newInstance( OutputVisualizationTuple tuple, Visualization v, Modality m,
         ClientSideSession session ) throws Exception {

      if( tuple == null || tuple.controllerClass == null )
         return null;

      Constructor<GenericController> constructor = (Constructor<GenericController>)tuple.controllerClass.getConstructor(
            Visualization.class, Modality.class, ClientSideSession.class, String.class );

      GenericController instantiation = (GenericController)constructor.newInstance( v, m, session, tuple.output );

      System.out.println( "Instantiated visualizationview: " + instantiation + " for output type " + tuple.output );
      return instantiation;
   }

   public DataModel getDataModel() {
      return dataModel;
   }

   public JPanel getParameterPanel() {
      if( parameterView == null || parameterView.hasEditableParameters() == false )
         return null;

      return parameterView.getRenderPanel();
   }

   public JPanel getOrientationPanel() {
      if( orientationView == null )
         return null;
      return orientationView.getRenderPanel();
   }

   /**
    * This method should be called by overriding classes to initialize the gui
    * 
    */
   public void initializeComponents() {
      if( visualizationRenderer != null && visualizationRenderer.getRenderPanel() != null ) {
         add( visualizationRenderer.getRenderPanel(), BorderLayout.CENTER );
         visualizationRenderer.getRenderPanel().setCursor( Cursor.getPredefinedCursor( Cursor.CROSSHAIR_CURSOR ) );
      }

      if( statusBar != null )
         add( statusBar, BorderLayout.PAGE_END );

      this.invalidate();
   }

   /**
    * This method is called from the ViewManager after all parameters for the requested session are fetched from the
    * server. The initializeComponents above is too early for that.
    */
   public void initializeParameterComponents() {

      if( parameterView != null ) {
         parameterView.initializeComponents();
      }
   }

   public DataStore getDataStore() {
      return dataModel.getDataStore();
   }

   public void updateProgress( String algorithm, int progress ) {
      statusBar.setProgress( algorithm, progress );
   }

   public void updateData( byte[] data, int resolution, int xPos, int yPos ) {
      if( dataModel != null )
         dataModel.updateData( data, resolution, xPos, yPos );

      if( visualizationRenderer != null )
         visualizationRenderer.display();

      if( orientationView != null )
         orientationView.display();

      if( parameterView != null )
         parameterView.display();
   }

   public void clearDataStore() {
      dataModel.clearDataStore();
   }

   public abstract void createNewDataStore( DataObject dataObject );

   public void updateParameterValues( ArrayList<ParameterValue> parameterValues, boolean updateRequiresRender ) {
      dataModel.updateParameterValues( parameterValues, updateRequiresRender );
   }

   public void setParameters( ArrayList<Parameter> params ) {
      dataModel.setParameters( params );
   }

   public ConcurrentHashMap<String, Parameter> getParameters() {
      return dataModel.getParameters();
   }

   public ArrayList<Parameter> getParameterSnapshot() {
      return dataModel.getParameterSnapshot();
   }

   public ViewStatusBar getStatusBar() {
      return statusBar;
   }

   /**
    * Interface implementations below
    */
   public void parameterViewChanged() {
      requestNewRender();
   }

   public void setFrameTitle( String title ) {
      Application.viewManager.setFrameTitle( this, title );
   }

   /**
    * This method checks whether this GenericController and the target GenericController are synchronizable. Two
    * controllers are synchronizable when they both have exactly the same set of synchronizable parameters.
    * 
    * @param target
    * @return
    */
   public boolean isSynchronizableWith( GenericController target ) {
      // we should not synchronize to ourselves to prevent loops
      if( target == this )
         return false;

      ConcurrentHashMap<String, Parameter> targetParams = target.getParameters();

      // unfortunately, we need to check both directions, it is possible that some controller has a stricter policy on
      // parameters required for the synchronization than the other
      for( Parameter p : dataModel.getParameters().values() ) {
         if( p.requiredForSynchronization() ) {
            // first look up the correct key
            Parameter targetParam = targetParams.get( p.getParameterName() );
            if( targetParam == null || targetParam.equals( p ) == false )
               return false;
         }
      }

      for( Parameter p : targetParams.values() ) {
         if( p.requiredForSynchronization() ) {
            // first look up the correct key
            Parameter targetParam = getParameters().get( p.getParameterName() );
            if( targetParam == null || targetParam.equals( p ) == false )
               return false;
         }
      }
      return true;
   }

   /**
    * This method will synchronize this GenericController to some specified controller
    * 
    * @param target
    */
   public void synchronizeWith( GenericController target ) {
      // sanity check: first check if the provided controllers can really be synchronized
      if( Application.viewManager.isSynchronizableWith( this, target ) == false ) {
         System.out.println( "Warning: cannot synchronize " + this + " with " + target );
         return;
      }

      ConcurrentHashMap<String, Parameter> targetParams = target.getParameters();
      ConcurrentHashMap<String, Parameter> ourParams = dataModel.getParameters();
      Parameter sourceParam = null;

      for( Parameter p : targetParams.values() ) {
         if( p.requiredForSynchronization() ) {
            sourceParam = ourParams.get( p.getParameterName() );
            if( sourceParam != null ) {
               sourceParam.setParameterValue( p.getParameterValue() );
               sourceParam.setModified( true );
               // notify all listeners that the parameter has changed
               dataModel.forceParameterUpdateNotification( sourceParam );
            }
         }
      }
      requestNewRender();
   }

   /**
    * This method will save the current visualization as a lookmark to be retrieved at a later stage
    * 
    * @return true if the lookmark was saved, false if a render was still in progress
    */
   public boolean createLookmark( boolean isPersonal, String description ) {
      if( needsUpdate ) {
         Application.showErrorMessage( "Lookmark creation error", "Render still in progress..." );
         return false;
      }

      // make sure no new render is started
      synchronized( updateLockObject ) {
         try {
            session.createUserDefinedLookmark( isPersonal, description );
         }
         catch( Exception e ) {
            Application.showErrorMessage( "Lookmark creation error", e.toString() );
         }
      }
      return true;
   }

}
